using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

namespace SpellCore
{
    public class ASpellWrapper : ISpellCheckEngine, IDisposable
    {
        private static IntPtr _aspellLibrary;
        private IntPtr _spellConfig;
        private IntPtr _spellChecker;

        private const string DATA_CHARSET_FILE = "iso8859-1.dat";
        private const string DATA_KEYBOARD_FILE = "standard.kbd";
        private const string SEED_DICTIONARY_DIR = "seed_dictionary";
        private const string DATA_PHONETICS_FILE_SUFFIX = "_phonet.dat";
        private const string ASPELL_DICTIONARY_SUFFIX = ".rws";
        private const string SEED_DICTIONARY_FILE = "empty" + ASPELL_DICTIONARY_SUFFIX;
        private const string ASPELL_MULTI_FILE_SUFFIX = ".multi";
       
        [DllImport("kernel32.dll")]
        public static extern IntPtr LoadLibrary(string lpFileName);

        public static ASpellWrapper CreateWrapper(string pathToEngineRootDirectory, string pathToMasterDirForWS,
                                                  string userPathToUserDirForWSrDir, string writingSystemId)
        {
            if (string.IsNullOrEmpty(writingSystemId))
            {
                throw new ArgumentException("Writing System Id cannot be null or empty", writingSystemId);
            }
            string publicDir = Path.Combine(pathToMasterDirForWS, "aspell");
            string privateDir = Path.Combine(userPathToUserDirForWSrDir, "aspell");

            if (_aspellLibrary == IntPtr.Zero)
            {
                _aspellLibrary = LoadLibrary(Path.Combine(pathToEngineRootDirectory, "aspell.dll"));
            }

            if (_aspellLibrary == IntPtr.Zero)
            {
                return null;
            }

            ASpellWrapper aspellWrapper;
            try
            {
                aspellWrapper =
                        new ASpellWrapper(writingSystemId, publicDir, privateDir);
            }
            catch
            {
                return null;
            }
            return aspellWrapper;
        }

        [DllImport("aspell.dll")]
        //returns AspellKeyInfoEnumeration *
        private static extern IntPtr aspell_config_possible_elements(IntPtr aspellConfig, int include_extra);

        [DllImport("aspell.dll")]
        private static extern int aspell_key_info_enumeration_at_end(IntPtr sspellKeyInfoEnumeration);

        [DllImport("aspell.dll")]
        //returns AspellKeyInfo *
        private static extern IntPtr aspell_key_info_enumeration_next(IntPtr aspellKeyInfoEnumeration);

        [DllImport("aspell.dll")]
        private static extern void delete_aspell_key_info_enumeration(IntPtr aspellKeyInfoEnumeration);

        private ASpellWrapper(string writingSystemId, string publicDir, string privateDir)
        {
            if (!Directory.Exists(publicDir))
            {
                throw new DirectoryNotFoundException(publicDir + " does not exist.");
            }
            EnsurePersonalDictionaryExists(privateDir);

            Regex regex = new Regex("\\-");
            writingSystemId = regex.Replace(writingSystemId, "_");

            Dictionary<String, String> options = new Dictionary<string, string>();

            options.Add("lang", writingSystemId);
            options.Add("dict-dir", publicDir);
            options.Add("master", writingSystemId);
            //path to where the personal files will be written
            options.Add("home-dir", privateDir);
            options.Add("personal", "personal.pws");
            options.Add("repl", "personal.prepl");
            //MUST HAVE THIS DIRECTORY for aspell to work correctly!
            //it must have the right data files for the language(s) you're using
            options.Add("data-dir", Path.Combine(publicDir, "data"));

            options.Add("encoding", "utf-8");
            options.Add("normalize", "true");
            options.Add("norm-strict", "true");

            options.Add("norm-form", "nfc");
            options.Add("norm-required", "true");

            _spellConfig = new_aspell_config();
            foreach (KeyValuePair<string, string> pair in options)
            {
                using (Utf8Marshaller keyMarshaller = new Utf8Marshaller(pair.Key))
                {
                    using (Utf8Marshaller valueMarshaller = new Utf8Marshaller(pair.Value))
                    {
                        aspell_config_replace(_spellConfig,
                                              keyMarshaller.MarshalledValue,
                                              valueMarshaller.MarshalledValue);
                    }
                }
            }

            //naming format for dictionaries: language[_region][-variety][-size]ASPELL_MULTI_FILE_SUFFIX

            IntPtr possibleError = new_aspell_speller(_spellConfig);
            if (aspell_error_number(possibleError) != 0)
            {
                string error = Utf8Marshaller.MarshalFromUtf8(aspell_error_message(possibleError));
                throw new ApplicationException(error);
            }
            else
            {
                _spellChecker = to_aspell_speller(possibleError);
            }
            if (_spellChecker == IntPtr.Zero)
            {
                 throw new ApplicationException("Couldn't create aspell session.");
            }

        }

        private static void EnsurePersonalDictionaryExists(string privateDir)
        {
            Directory.CreateDirectory(privateDir);
        }

        public static void CreateMasterDictionary(string pathToEngineRootDirectory, string pathToMasterDirForWS, string userPathToUserDirForWSrDir, string writingSystemId)
        {
            string publicDir = Path.Combine(pathToMasterDirForWS, "aspell");
            string privateDir = Path.Combine(userPathToUserDirForWSrDir, "aspell");

            if (Directory.Exists(publicDir))
            {
                throw new InvalidOperationException(publicDir + " already exists.");
            }
            Directory.CreateDirectory(publicDir);

            pathToEngineRootDirectory = Path.Combine(pathToEngineRootDirectory, "..");
            string seedDictionaryDir = Path.Combine(pathToEngineRootDirectory, SEED_DICTIONARY_DIR);
                
            string[] writingSystemParts = writingSystemId.Split('_');

            string dictionaryFileName = writingSystemId + ASPELL_DICTIONARY_SUFFIX;
            if (!File.Exists(Path.Combine(publicDir,dictionaryFileName)))
            {
                //copy SEED_DICTIONARY_FILE to writingSystemId.rws in the right directory
                File.Copy(Path.Combine(seedDictionaryDir, SEED_DICTIONARY_FILE),
                          Path.Combine(publicDir,dictionaryFileName));
            }
            string multiDictionaryFilePath = Path.Combine(publicDir,writingSystemId+ASPELL_MULTI_FILE_SUFFIX);
            if (!File.Exists(multiDictionaryFilePath))
            {
                File.WriteAllLines(multiDictionaryFilePath,
                      new string[]{"# Generated with Aspell Dicts \"proc\" script version 0.50.1",
                                   "strip-accents false",
                                   "add " + dictionaryFileName});
            }

            string dataDirectory = Path.Combine(publicDir, "data");
            Directory.CreateDirectory(dataDirectory);

            writingSystemParts[0] = "en";
            string datFileName = writingSystemParts[0] + ".dat";
            if (!File.Exists(Path.Combine(dataDirectory, datFileName)))
            {
                File.WriteAllLines(Path.Combine(dataDirectory, datFileName),
                        new string[]{"# Generated with Aspell Dicts \"proc\" script version 0.50.1",
                                     "name " + writingSystemParts[0],
                                     "charset iso8859-1",
                                     "soundslike " + writingSystemParts[0]});
            }
            string phoneticsFilePath = Path.Combine(dataDirectory, writingSystemParts[0] + DATA_PHONETICS_FILE_SUFFIX);
            if (!File.Exists(phoneticsFilePath))
            {
                File.WriteAllLines(phoneticsFilePath,
                        new string[]{"#   empty phonetics file, appears aspell needs this whether it uses it or not",
                                     "version 1.1"});
            }
            
            CopyFileFromSeedData(DATA_CHARSET_FILE, seedDictionaryDir, publicDir);
            CopyFileFromSeedData(DATA_KEYBOARD_FILE, seedDictionaryDir, publicDir);

            EnsurePersonalDictionaryExists(privateDir);
        }
        
        static void CopyFileFromSeedData(string fileName, string seedDir, string publicDir)
        {
            string relativeFilePath = Path.Combine("data", fileName);
            string destinationFilePath = Path.Combine(publicDir, relativeFilePath);
            if (!File.Exists(destinationFilePath))
            {
                File.Copy(Path.Combine(seedDir, relativeFilePath), destinationFilePath);
            }
        }

        [DllImport("aspell.dll")]
        //returns AspellConfig *
        private static extern IntPtr new_aspell_config();

        [DllImport("aspell.dll")]
        private static extern void delete_aspell_config(IntPtr aspellConfig);

        [DllImport("aspell.dll")]
        private static extern void aspell_config_clone(IntPtr aspellConfig);

        [DllImport("aspell.dll")]
        private static extern int aspell_config_error_number(IntPtr aspellConfig);

        [DllImport("aspell.dll")]
        //returns char *
        private static extern IntPtr aspell_config_error_message(IntPtr aspellConfig);

        [DllImport("aspell.dll")]
        //returns AspellError *
        private static extern IntPtr aspell_config_error(IntPtr aspellConfig);

        /* inserts an item, if the item already exists it
 * will be replaced. returns true if it succesed
 * or false on error. If the key in not valid it
 * sets error_num to PERROR_UNKNOWN_KEY, if the
 * value is not valid it will sets error_num to
 * PERROR_BAD_VALUE, if the value can not be
 * changed it sets error_num to
 * PERROR_CANT_CHANGE_VALUE, and if the value is
 * a list and you are trying to set it directory
 * it sets error_num to PERROR_LIST_SET */

        [DllImport("aspell.dll")]
        private static extern int aspell_config_replace(IntPtr aspellConfig, IntPtr key, IntPtr value);

        [DllImport("aspell.dll")]
        //returns AspellCanHaveError * 
        private static extern IntPtr new_aspell_speller(IntPtr aspellConfig);

        [DllImport("aspell.dll")]
        //returns AspellSpeller *
        private static extern IntPtr to_aspell_speller(IntPtr canHaveError);

        [DllImport("aspell.dll")]
        private static extern void delete_aspell_speller(IntPtr aspellSpeller);

        [DllImport("aspell.dll")]
        private static extern int aspell_speller_error_number(IntPtr aspellSpeller);

        [DllImport("aspell.dll")]
        private static extern IntPtr aspell_speller_error_message(IntPtr aspellSpeller);

        [DllImport("aspell.dll")]
        //returns AspellError *
        private static extern IntPtr aspell_speller_error(IntPtr aspellSpeller);

        [DllImport("aspell.dll")]
        //returns AspellConfig *
        private static extern IntPtr aspell_speller_config(IntPtr aspellSpeller);

        [DllImport("aspell.dll")]
        /* returns  0 if it is not in the dictionary,
          * 1 if it is, or -1 on error. */
        private static extern int aspell_speller_check(IntPtr aspellSpeller, IntPtr word, int word_size);

        [DllImport("aspell.dll")]
        private static extern int aspell_error_number(IntPtr aspellError);

        [DllImport("aspell.dll")]
        //character string pointer
        private static extern IntPtr aspell_error_message(IntPtr aspellError);

        [DllImport("aspell.dll")]
        //returns AspellError *
        private static extern IntPtr aspell_error(IntPtr aspellError);

        [DllImport("aspell.dll")]
        private static extern void delete_aspell_can_have_error(IntPtr aspellError);

        #region ICorrectnessProvider Members

        public CheckingResult GetWordStatus(string word)
        {
            if (IsWordSpelledCorrect(word.Normalize(NormalizationForm.FormC))
                || IsWordSpelledCorrect(word.Normalize(NormalizationForm.FormD)))
            {
                return CheckingResult.Good;
            }
            return CheckingResult.Unknown;
        }

        private bool IsWordSpelledCorrect(string word)
        {
            int isCorrect;
            using (Utf8Marshaller marshalledWord = new Utf8Marshaller(word))
            {
                isCorrect = aspell_speller_check(_spellChecker, marshalledWord.MarshalledValue, -1);
                if (isCorrect == -1)
                {
                    string errorMessage = Utf8Marshaller.MarshalFromUtf8(aspell_speller_error_message(_spellChecker));
                    throw new ExternalException(errorMessage);
                }
            }
            return (isCorrect != 0 ? true : false);
        }

        public bool CanSupportAdditions()
        {
            return true;
        }

        public void AddWordAsGood(string word)
        {
            using (Utf8Marshaller marshalledWord = new Utf8Marshaller(word.Normalize(NormalizationForm.FormC)))
            {
                int isError =
                        aspell_speller_add_to_personal(_spellChecker,
                                                       marshalledWord.MarshalledValue,
                                                       Encoding.UTF8.GetByteCount(word));
                if (isError == -1)
                {
                    string errorMessage = Utf8Marshaller.MarshalFromUtf8(aspell_speller_error_message(_spellChecker));
                    throw new ExternalException(errorMessage);
                }
                isError = aspell_speller_save_all_word_lists(_spellChecker);
                if (isError == -1)
                {
                    string errorMessage = Utf8Marshaller.MarshalFromUtf8(aspell_speller_error_message(_spellChecker));
                    throw new ExternalException(errorMessage);
                }
            }
        }

        public bool CanSupportBadWords()
        {
            return false;
        }

        #region ISpellCheckEngine Members

        /// <summary>
        /// Adds a word to the known bad word store
        /// </summary>
        /// <param name="word"></param>
        /// <exception cref="NotSupportedException">When CanSupportBadWords is false</exception>
        public void AddWordAsBad(string word)
        {
            throw new NotImplementedException();
        }

        #endregion

        [DllImport("aspell.dll")]
        private static extern int aspell_speller_add_to_personal(IntPtr aspellSpeller, IntPtr word, int word_size);

        [DllImport("aspell.dll")]
        private static extern int aspell_speller_save_all_word_lists(IntPtr aspellSpeller);

        #endregion

        #region Suggestions Members

        public bool CanProvideSuggestions()
        {
            return true;
        }

        public IEnumerable<SimilarityCheckResult> GetSuggestions(string word)
        {
            List<SimilarityCheckResult> suggestions = new List<SimilarityCheckResult>();
            using (Utf8Marshaller marshalledWord = new Utf8Marshaller(word))
            {
                int byteCount = Encoding.UTF8.GetByteCount(word);
                IntPtr suggestionsPtr =
                        aspell_speller_suggest(_spellChecker, marshalledWord.MarshalledValue, byteCount);
                IntPtr wordListPtr = aspell_word_list_elements(suggestionsPtr);

                string suggestion = Utf8Marshaller.MarshalFromUtf8(aspell_string_enumeration_next(wordListPtr));
                while (!String.IsNullOrEmpty(suggestion))
                {
                    SimilarityCheckResult result =
                            new SimilarityCheckResult(SimilarityCheckResult.UnknownScore, suggestion);
                    suggestions.Add(result);
                    suggestion = Utf8Marshaller.MarshalFromUtf8(aspell_string_enumeration_next(wordListPtr));
                }
                delete_aspell_string_enumeration(wordListPtr);
            }
            return suggestions;
        }

        /* Return null on error.
 * the word list returned by suggest is only valid until the next
 * call to suggest */

        [DllImport("aspell.dll")]
        //returns AspellWordList *
        private static extern IntPtr aspell_speller_suggest(IntPtr aspellSpeller, IntPtr word, int word_size);

        [DllImport("aspell.dll")]
        //returns AspellStringEnumeration *
        private static extern IntPtr aspell_word_list_elements(IntPtr aspellWordList);

        /*[DllImport("aspell.dll")]
        private extern static int aspell_speller_store_replacement(IntPtr aspellSpeller, IntPtr misspelledWord,
                                                                    int mis_size, IntPtr goodWord, int cor_size);*/

        [DllImport("aspell.dll")]
        private static extern void delete_aspell_string_enumeration(IntPtr aspellStringEnumeration);

        [DllImport("aspell.dll")]
        //returns const char *
        private static extern IntPtr aspell_string_enumeration_next(IntPtr aspellStringEnumeration);

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private bool _isDisposed;

        private void Dispose(bool disposing)
        {
            if (_isDisposed)
            {
                return;
            }

            _isDisposed = true;
            if (disposing)
            {
                //dump managed resources
            }

            if (_spellChecker != IntPtr.Zero)
            {
                int isError = aspell_speller_save_all_word_lists(_spellChecker);
                if (isError == -1)
                {
                    string errorMessage =
                            Utf8Marshaller.MarshalFromUtf8(aspell_speller_error_message(_spellChecker));
                    throw new ExternalException(errorMessage);
                }
            }
            //can't delete here as it seems to generate an program crash
            if (_spellChecker != IntPtr.Zero)
            {
                delete_aspell_speller(_spellChecker);
            }
            if (_spellChecker != IntPtr.Zero)
            {
                delete_aspell_config(_spellConfig);
            }
        }

        ~ASpellWrapper()
        {
            Dispose(false);
        }

        #endregion
    }
}
